﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using snat.proxy;
using snat.proxy.sqlapi;
using System.Threading.Tasks;
using snat.proxy.items;
using snat.Exceptions;

namespace snat.model {

    /// <summary>
    /// Delegate type for tape events
    /// </summary>
    public delegate void systemHandler();
    /// <summary>
    /// Delegate type for message processed
    /// </summary>
    /// <param name="m">Message that has been processed</param>
    public delegate void messageDelegate(Message m);
    /// <summary>
    /// Delegate type for a node added event
    /// </summary>
    /// <param name="id">Network model ID of the node</param>
    /// <param name="b">The added node</param>
    public delegate void nodeDelegate(int id, BaseNode b);
    /// <summary>
    /// Delegate type for edge added event
    /// </summary>
    /// <param name="e">The added edge</param>
    public delegate void edgeDelegate(Edge e);

    public sealed class NetworkModel {
        #region EVENTS

        /// <summary>
        /// True if the tape has been initialized, false otherwise
        /// </summary>
        public bool TapeHasLoaded { get; private set; }
        /// <summary>
        /// Event thrown when the tape has been fully initialized
        /// </summary>
        public event systemHandler TapeLoaded;
        private void InitFinished() {
            TapeHasLoaded = true;
            if (TapeLoaded != null) TapeLoaded();
        }
        /// <summary>
        /// Event thrown when the tape is first played
        /// </summary>
        public bool TapeHasStarted { get; private set; }
        public event systemHandler TapeStarted;
        private void TapeFirstPlayed() {
            TapeHasStarted = true;
            if (TapeStarted != null) TapeStarted();
        }
        /// <summary>
        /// Event thrown when the tape is played
        /// </summary>
        public event systemHandler TapeRunning;
        private void TapeNowRunning() {
            if (TapeRunning != null) TapeRunning();
        }
        /// <summary>
        /// Event thrown when the tape is paused
        /// </summary>
        public bool TapeIsPaused { get; private set; }
        public event systemHandler TapePaused;
        private void PauseCalled() {
            if (TapePaused != null) TapePaused();
        }
        /// <summary>
        /// Event thrown when the end of tape has been reached
        /// </summary>
        public bool TapeHasEnded { get; private set; }
        public event systemHandler TapeEnded;
        private void EndOfTape() {
            TapeHasEnded = true;
            if (TapeEnded != null) TapeEnded();
        }
        /// <summary>
        /// Event thrown when a message has been processed by the network model
        /// </summary>
        public event messageDelegate MessageProcessed;
        private void MessageSent(Message m) {
            if (MessageProcessed != null) MessageProcessed(m);
        }
        /// <summary>
        /// Event thrown when a new node is added to the network model
        /// </summary>
        public event nodeDelegate NodeAdded;
        private void AddNode(int id, BaseNode b) {
            if (NodeAdded != null && b != null) NodeAdded(id, b);
        }
        /// <summary>
        /// Event thrown when a new edge is added to the network model
        /// </summary>
        public event edgeDelegate EdgeAdded;
        private void AddEdge(Edge e) {
            if (EdgeAdded != null &&
                Nodes.ContainsKey(e.Source.ID) &&
                Nodes.ContainsKey(e.Target.ID)) EdgeAdded(e);
        }
        #endregion

        /// <summary>
        /// Number of messages that have been processed in the network model
        /// </summary>
        public int MessageProcessedCount { get; private set; }
        /// <summary>
        /// Total number of messages held in the database when a node factory is in use filtering nodes
        /// </summary>
        /// <remarks>
        /// This may be different to the number of messages on the tape. 
        /// This is because some messages are ignored if nodes are filtered from the database
        /// </remarks>
        private int TotalMessages;
        /// <summary>
        /// Number of messages skipped from the database due to nodes not present in the network model
        /// </summary>
        private int MessagesNotProcessedCount;
        /// <summary>
        /// Percentage of the database messages iterated over
        /// </summary>
        public double TapePercentagePlayed {
            get {
                if (TotalMessages == 0 || TapeHasEnded) {
                    return 1;
                }
                return (double)(MessageProcessedCount + MessagesNotProcessedCount) / (double)TotalMessages;
            }
        }
        /// <summary>
        /// Network model NodeID -> the node
        /// </summary>
        public Dictionary<int, BaseNode> Nodes { get; private set; }
        /// <summary>
        /// Database EntityID -> the node
        /// </summary>
        private Dictionary<int, BaseNode> DBNodes;
        /// <summary>
        /// Get's the ID of the Entity on the database, given a NetworkModel ID.
        /// </summary>
        /// <param name="baseNodeID"></param>
        /// <remarks>Can be used in getting messages regarding a node from database <see cref="BaseNode.MessagesSent"/></remarks>
        /// <returns>ID of Entity representing baseNodeID on database</returns>
        internal List<int> ConvertToDatabaseIDs(int baseNodeID) {
            return DBNodes.Keys.Where(e => DBNodes[e].ID == baseNodeID).ToList();
        }
        /// <summary>
        /// Get's the network model ID of the Node in the NetworkModel, given a Database ID.
        /// </summary>
        /// <param name="baseNodeID"></param>
        /// <remarks>Can be used in getting messages regarding a node from database <see cref="BaseNode.MessagesSent"/></remarks>
        /// <returns>ID of Entity representing baseNodeID on NetworkModel</returns>
        internal List<BaseNode> ConvertToNetworkModelIDs(List<int> databaseIDs) {
            return databaseIDs.Intersect(DBNodes.Keys).ToList().ConvertAll<BaseNode>(dbid => DBNodes[dbid]);
        }
        /// <summary>
        /// Get's the Node in the NetworkModel, given a Database ID.
        /// </summary>
        /// <param name="baseNodeID"></param>
        /// <remarks>Can be used in getting messages regarding a node from database <see cref="BaseNode.MessagesSent"/></remarks>
        /// <returns>Node in NetworkModel</returns>
        internal BaseNode ConvertToNetworkModelID(int databaseID) {
            if (DBNodes.ContainsKey(databaseID)) return DBNodes[databaseID];
            return null;
        }

        /// <summary>
        /// Edges stored as a hash set in the network model
        /// </summary>
        private HashSet<Edge> edges;
        /// <summary>
        /// List of edges in the network model
        /// </summary>
        public List<Edge> Edges {
            get {
                return edges.ToList();
            }
        }

        /// <summary>
        /// Singleton code for NetworkModel
        /// </summary>
        private static volatile NetworkModel instance = new NetworkModel();
        /// <summary>
        /// Instance of NetworkModel - there can only be one in the process
        /// </summary>
        public static NetworkModel Instance {
            get {
                return instance;
            }
        }

        /// <summary>
        /// Earliest timestamp of message on tape
        /// </summary>
        public DateTime StartTime { get; private set; }
        /// <summary>
        /// Latest timestamp of message on tape
        /// </summary>
        public DateTime EndTime { get; private set; }
        /// <summary>
        /// Timestamp of last message processed in network model
        /// </summary>
        public DateTime CurrentTime { get; private set; }
        /// <summary>
        /// The message iterator (the tape)
        /// </summary>
        private IEnumerator<DBMessage> messageIterator;

        /// <summary>
        /// Contructor used by singleton - instantiates objects in the class
        /// </summary>
        private NetworkModel() {
            Nodes = new Dictionary<int, BaseNode>();
            edges = new HashSet<Edge>();
            DBNodes = new Dictionary<int, BaseNode>();
            messageIterator = new List<DBMessage>().GetEnumerator();
            NodeAdded += OnNodeAdded;
            EdgeAdded += OnEdgeAdded;
            TapeEnded += OnTapeEnded;
        }

        /// <summary>
        /// Connects the database proxy to a database
        /// </summary>
        /// <param name="url">Host server of the database</param>
        /// <param name="port">Port of the database</param>
        /// <param name="database">Name of the database</param>
        /// <param name="username">Username for database server</param>
        /// <param name="password">Password for database server</param>
        public void Connect(String url, int port, String database, String username, String password) {
            DataProxy.Instance.Connect(url, port, database, username, password);
        }

        #region INITIALIZATION
        private bool NodesHaveInitialized = false;

        /// <summary>
        /// Short-hand using default a base node factory, fully initializes the network model
        /// </summary>
        /// <remarks>Only safe to use with no algorithms registered, or if the algorithms are network based</remarks>
        public void InitAll() {
            InitAll(new BaseNodeFactory());
        }

        /// <summary>
        /// Short-hand which fully inializes the network model
        /// </summary>
        /// <remarks>Only safe to use with no algorithms registered, or if the algorithms are network based</remarks>
        /// <param name="factory">Node factory to use in populating nodes in the network model</param>
        public void InitAll(INodeFactory factory) {
            InitNodes(factory);
            Init();
        }

        /// <summary>
        /// Initializes the network model edges and message iterator
        /// </summary>
        /// <remarks>Will throw exception if called before InitNodes</remarks>
        public void Init() {
            if (!NodesHaveInitialized) {
                throw new NodesNotInitializedException(@"The nodes have not yet been initialized,
                                                        make a call to NetworkModel.Instance.NodeInit(..) before calling
                                                        NetworkModel.Instance.Init().");
            }
            edges.Clear();
            messageIterator.Dispose();
            TapeHasEnded = false;
            TapeIsPaused = true;
            MessageProcessedCount = 0;
            MessagesNotProcessedCount = 0;

            InitializeEdges();

            InitializeMessages();

            Console.WriteLine("SNAT :: done.");
        }

        /// <summary>
        /// Resets the message iterator and the edges in the network model
        /// </summary>
        /// <remarks>Does not reset node state in case node based algorithm is used, cluster id for example</remarks>
        public void Reset() {
            edges.Clear();
            messageIterator.Dispose();
            TapeHasEnded = false;
            TapeIsPaused = true;
            MessageProcessedCount = 0;
            MessagesNotProcessedCount = 0;

            foreach (BaseNode node in Nodes.Values) {
                node.InEdges.Clear();
                node.OutEdges.Clear();
            }

            InitializeEdges();

            InitializeMessages();

            Console.WriteLine("SNAT :: done.");
        }

        /// <summary>
        /// Populates the network model with nodes using a base node factory
        /// </summary>
        public void InitNodes() {
            InitNodes(new BaseNodeFactory());
        }

        /// <summary>
        /// Populates the network model with nodes
        /// </summary>
        /// <param name="nodeFactory">Node factory to use in populating the nodes</param>
        public void InitNodes(INodeFactory nodeFactory) {
            Nodes.Clear();
            DBNodes.Clear();
            Console.WriteLine("Initializing the nodes...");
            ListProxy<DBEntity> allEntities = EntityQuery.All();
            // End the tape if no nodes are in the database
            if (allEntities.Count == 0) {
                throw new NoNodesException("There were no nodes found in the database schema, and therefore no network to model");
                EndOfTape();
                return;
            } else {
                // Iterate through Entity table in database using list proxy
                foreach (DBEntity e in allEntities) {
                    BaseNode node = nodeFactory.Instantiate(e); // Instantiate the node with the node factory
                    if (node != null) {
                        Console.WriteLine("Added Node " + node.Name);
                        DBNodes.Add(e.EntityID, node);
                        if (!Nodes.ContainsKey(node.ID)) {
                            AddNode(node.ID, node);
                        }
                    }
                }
                NodesHaveInitialized = true;
            }
        }

        /// <summary>
        /// Populates the network model with edges
        /// </summary>
        private void InitializeEdges() {
            Console.WriteLine("Initializing the edges...");
            ListProxy<DBConnection> initialConnections = ConnectionQuery.Init();
            foreach (DBConnection c in initialConnections) {
                // Both ends of the edge must exist in the network model
                if (DBNodes.ContainsKey(c.FromEntity.EntityID) && DBNodes.ContainsKey(c.ToEntity.EntityID)) {
                    AddEdge(new Edge(DBNodes[c.FromEntity.EntityID], DBNodes[c.ToEntity.EntityID], DateTime.MinValue));
                }
            }
        }

        /// <summary>
        /// Initializes the network model tape (message iterator)
        /// </summary>
        private void InitializeMessages() {
            ListProxy<DBMessage> messages = MessageQuery.All();
            TotalMessages = messages.Count;
            messageIterator = messages.GetEnumerator();

            if (MoveNextMessage()) {
                StartTime = messageIterator.Current.Timestamp;
                CurrentTime = messageIterator.Current.Timestamp;
                EndTime = MessageQuery.LastMessage().Timestamp;
                InitFinished();
            } else {
                messageIterator = new List<DBMessage>().GetEnumerator();
                StartTime = DateTime.Now;
                CurrentTime = DateTime.Now;
                EndTime = DateTime.Now;
                InitFinished();
                EndOfTape();
            }
        }
        #endregion

        /// <summary>
        /// Moves the message iterator onto the next valid message
        /// </summary>
        /// <returns>True if a valid message was found, false otherwise</returns>
        private bool MoveNextMessage() {
            while (messageIterator.MoveNext()) {
                //if it is a valid message (sender and a recipient exist in NM)
                if (DBNodes.ContainsKey(messageIterator.Current.Sender.EntityID) &&
                    (messageIterator.Current.Recipients.Count == 0 ||
                    messageIterator.Current.Recipients.Any(e => DBNodes.ContainsKey(e.EntityID)))) {
                    return true;    //there is a valid message
                }
                MessagesNotProcessedCount++;
            }
            return false;   //no more valid messages
        }

        #region TAPE METHODS
        /// <summary>
        /// Lock to stop tape methods being run at the same time
        /// </summary>
        private object playingLock = new object();

        /// <summary>
        /// Plays the tape to the end unless Pause is called
        /// </summary>
        public void Play() {
            lock (playingLock) {
                TapeIsPaused = false;
                if (!TapeHasStarted) TapeFirstPlayed();
                TapeNowRunning();
                while (!TapeIsPaused && !TapeHasEnded) {
                    StepOne();
                }
            }
        }

        /// <summary>
        /// Steps the tape along specified amount
        /// </summary>
        /// <param name="steps">Number of messages to step through</param>
        public void Step(int steps) {
            lock (playingLock) {
                TapeIsPaused = false;
                if (!TapeHasStarted) TapeFirstPlayed();
                TapeNowRunning();
                while (steps-- > 0 && !TapeIsPaused && !TapeHasEnded) {
                    StepOne();
                }
                if (!TapeIsPaused) {
                    Pause();
                }
            }
        }

        /// <summary>
        /// Steps the tape along specified amount
        /// </summary>
        /// <param name="date">Date to step the network model up until</param>
        public void Step(DateTime date) {
            lock (playingLock) {
                TapeIsPaused = false;
                if (!TapeHasStarted) TapeFirstPlayed();
                TapeNowRunning();
                while (CurrentTime < date && !TapeIsPaused && !TapeHasEnded) {
                    StepOne();
                }
                if (!TapeIsPaused) {
                    Pause();
                }
            }
        }

        /// <summary>
        /// Steps the tape along specified amount
        /// </summary>
        /// <param name="time">Amount of time to step by</param>
        public void Step(TimeSpan time) {
            Step(CurrentTime + time);
        }

        /// <summary>
        /// Modes the tape on one message
        /// </summary>
        private void StepOne() {
            if (messageIterator.Current == null) {
                TapeIsPaused = true;
                throw new InvalidOperationException("The tape has either finished or never existed, you cannot step forwards");
            }
            // Get the sender as a network model node rather than entity
            BaseNode sender = DBNodes[messageIterator.Current.Sender.EntityID];
            // Remove those recipients which do not appear in the network model
            messageIterator.Current.Recipients.RemoveAll(e => !DBNodes.ContainsKey(e.EntityID));
            // Get the recipients as network model nodes rather than entities
            List<BaseNode> recipients = messageIterator.Current.Recipients.ConvertAll<BaseNode>(e => DBNodes[e.EntityID]);
            /* Convert the message into a network model message from that provided by the list proxy iterator*/
            Message current = new Message(messageIterator.Current.MessageID,
                                          sender,
                                          recipients,
                                          messageIterator.Current.IDonNetwork,
                                          messageIterator.Current.Restricted,
                                          messageIterator.Current.Timestamp,
                                          messageIterator.Current.Data);

            MessageProcessedCount++;

            // Add all newly created edges
            foreach (BaseNode recipient in current.Recipients.ConvertAll<BaseNode>(e => Nodes[e.ID])) {
                Edge newEdge = new Edge(Nodes[current.Sender.ID], Nodes[recipient.ID], current.Timestamp);
                if (!edges.Contains(newEdge)) {
                    AddEdge(newEdge);
                }
            }

            CurrentTime = current.Timestamp;
            
            // Trigger send on the sender node
            Nodes[current.Sender.ID].TriggerSend(current);

            // Trigger received on all recipients of this message (Restricted means all current connections receive it)
            if (!messageIterator.Current.Restricted) {
                recipients = recipients.Union(Nodes[sender.ID].OutEdges.ConvertAll<BaseNode>(e => Nodes[e.Target.ID])).ToList();
            }
            Parallel.ForEach(recipients, r => {
                Nodes[r.ID].TriggerRecieve(current);
            });

            // Fire message processed event
            MessageSent(current);

            // move the tape along to next message
            if (!MoveNextMessage()) {
                EndOfTape();
                TapeIsPaused = true;
            }
        }

        /// <summary>
        /// Pause any playing of the tape
        /// </summary>
        public void Pause() {
            TapeIsPaused = true;
            PauseCalled();
        }

        /// <summary>
        /// Skip to the end of the tape, bypassing any messages - Gets the structure of the network only
        /// </summary>
        public void SkipAllMessages() {
            lock (playingLock) {
                Pause();
                ListProxy<DBConnection> allConnections = ConnectionQuery.All();
                foreach (DBConnection c in allConnections.Where<DBConnection>(
                        e => DBNodes.ContainsKey(e.FromEntity.EntityID) && DBNodes.ContainsKey(e.ToEntity.EntityID))) {
                    Edge newEdge = new Edge(DBNodes[c.FromEntity.EntityID], DBNodes[c.ToEntity.EntityID], CurrentTime, c.Strength);
                    if (!edges.Contains(newEdge)) {
                        AddEdge(newEdge);
                    }
                }
                messageIterator.Dispose();
                EndOfTape();
            }
        }
        #endregion

        private void OnEdgeAdded(Edge e) {
            Nodes[e.Source.ID].AddEdge(e);
            Nodes[e.Target.ID].AddEdge(e);
            edges.Add(e);
        }
        private void OnNodeAdded(int id, BaseNode n) {
            Nodes.Add(n.ID, n);
        }
        private void OnTapeEnded() {
            if (messageIterator != null) {
                messageIterator.Dispose();
            }
        }
    }
}
